/*
 *  Copyright (c) 2011 Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the GNU Lesser Public License v2.1
 *  which accompanies this distribution, and is available at
 *  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 *  Contributors:
 *      Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany - initial API and implementation
 */
package de.ipk_gatersleben.bit.bi.edal.primary_data;

import java.io.UnsupportedEncodingException;
import java.security.Policy;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.security.auth.Subject;

import de.ipk_gatersleben.bit.bi.edal.aspectj.security.GrantableMethods.Methods;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.EdalException;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.ImplementationProvider;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataDirectory;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataDirectoryException;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataEntity;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.implementation.ALLPrincipal;
import de.ipk_gatersleben.bit.bi.edal.primary_data.reference.CheckReviewStatusThread;
import de.ipk_gatersleben.bit.bi.edal.primary_data.reference.PersistentIdentifier;
import de.ipk_gatersleben.bit.bi.edal.primary_data.security.EdalCompositePolicy;
import de.ipk_gatersleben.bit.bi.edal.primary_data.security.EdalPolicy;

/**
 * Central entry point for the clients to make use of the API provided storage
 * functionality.
 * 
 * @author lange
 * @author arendd
 */
public class DataManager {

	/**
	 * Private class to provide a intercepter that control a graceful shutdown
	 * of API if some external kill the JVM.
	 * 
	 * @author lange
	 */
	private static class ShutdownInterceptor extends Thread {

		@Override
		public void run() {
			DataManager
					.getImplProv()
					.getLogger()
					.warn("eDAL: JVM got external kill signal - graceful shut down");
			DataManager.shutdown();
		}
	}

	private static CheckReviewStatusThread checkReviewStatusThread;
	private static EdalConfiguration configuration;
	/**
	 * Constant map with all initial default permissions.<br/>
	 * 
	 * {@link ALLPrincipal}:<br/>
	 * Methods.listPrimaryDataEntities<br/>
	 * Methods.listPrimaryDataEntitiesByDate<br/>
	 * Methods.getPrimaryDataEntity<br/>
	 * Methods.read<br/>
	 * Methods.exist<br/>
	 * Methods.getParentDirectory<br/>
	 * Methods.getVersions<br/>
	 * Methods.getCurrentVersion<br/>
	 * Methods.searchByDublinCoreElement<br/>
	 * Methods.searchByMetaData<br/>
	 */
	public static final Map<Principal, List<Methods>> DEFAULT_PERMISSIONS = new HashMap<Principal, List<Methods>>();

	private static ImplementationProvider implementationprovider;

	private static boolean isConfigurationValid = false;
	/**
	 * Time to wait between every check for root user confirmation
	 */
	private static final int OPT_IN_INTERVAL = 5000;
	/**
	 * The timeout to stop the root user confirmation
	 */
	private static final int OPT_IN_TIMEOUT = 1800000;
	/**
	 * eMail address for the ROOT CHEAT
	 */
	private static final String ROOT_CHEAT = "eDAL0815@ipk-gatersleben.de";
	/**
	 * Internal cheat to avoid the root login process.
	 */
	private static InternetAddress rootCheat;
	private static EdalJettyServer server;
	private static InheritableThreadLocal<Map<Principal, List<Methods>>> threadlocaldefaultpermissions;

	private static CountDownLatch stopLatch;

	/**
	 * Store the {@link Subject} and the {@link ImplementationProvider} of the
	 * getRootDirectory caller for later use, who is the working user of the
	 * eDAL data structure and what are the implementing classes. <br/>
	 * Store the defined userPermissions as thread local.
	 */
	private static InheritableThreadLocal<Subject> threadlocalsubject;

	static {
		DataManager.threadlocalsubject = new InheritableThreadLocal<>();
		DataManager.implementationprovider = null;
		DataManager.threadlocaldefaultpermissions = new InheritableThreadLocal<>();
		DataManager.configuration = null;
		DataManager.checkReviewStatusThread = new CheckReviewStatusThread();
		DataManager.server = null;

		// set initial default permissions
		final List<Methods> methods = new ArrayList<Methods>();

		methods.add(Methods.listPrimaryDataEntities);
		methods.add(Methods.listPrimaryDataEntitiesByDate);
		methods.add(Methods.getPrimaryDataEntity);
		methods.add(Methods.read);
		methods.add(Methods.exist);
		methods.add(Methods.getParentDirectory);
		methods.add(Methods.searchByDublinCoreElement);
		methods.add(Methods.searchByMetaData);

		DataManager.DEFAULT_PERMISSIONS.put(new ALLPrincipal(), methods);

		DataManager.resetDefaultPermissions();

		try {
			DataManager.rootCheat = new InternetAddress(ROOT_CHEAT);
		} catch (AddressException e) {
			e.printStackTrace();
		}
		DataManager.stopLatch = new CountDownLatch(1);

	}

	/**
	 * Getter for the available space in the mount path of eDAL.
	 * 
	 * @return available space
	 * @throws EdalException
	 *             if no path is specified.
	 */
	public static Long getAvailableStorageSpace() throws EdalException {
		if (DataManager.getImplProv() == null) {
			throw new EdalException(
					"No ImplementationProvider set --> run getRootDirectory()");
		}
		return DataManager.getImplProv().getAvailableStorageSpace();
	}

	/**
	 * Getter for the {@link EdalConfiguration}.
	 * 
	 * @return the current {@link EdalConfiguration}.
	 */
	public static EdalConfiguration getConfiguration() {
		return DataManager.configuration;
	}

	/**
	 * @return the current set default permissions.
	 */
	public static Map<Principal, List<Methods>> getDefaultPermissions() {
		return DataManager.threadlocaldefaultpermissions.get();
	}

	/**
	 * Getter for the current {@link ImplementationProvider}.
	 * 
	 * @return ImplementationProvider with all implementing classes
	 */
	public static ImplementationProvider getImplProv() {
		return DataManager.implementationprovider;
	}

	/**
	 * Get a specified {@link PrimaryDataEntity} for the request of a
	 * HTTPServer, but only if a PublicReference is defined.
	 * 
	 * @param uuid
	 *            the {@link UUID} of the {@link PrimaryDataEntity}.
	 * @param versionNumber
	 *            the version number of the
	 *            {@link de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataEntityVersion}
	 *            .
	 * @throws EdalException
	 *             if there is no {@link PrimaryDataEntity} with the specified
	 *             values or the eDAL system is not started.
	 * @return the specified {@link PrimaryDataEntity}
	 */
	static PrimaryDataEntity getPrimaryDataEntityByID(String uuid,
			long versionNumber) throws EdalException {

		if (getImplProv() == null || getSubject() == null) {
			throw new EdalException(
					"unable to load entity : start eDAL system first");
		}
		return DataManager.getImplProv().getPrimaryDataEntityByID(uuid,
				versionNumber);
	}

	static PrimaryDataEntity getPrimaryDataEntityForPersistenIdentifier(
			String uuid, long versionNumber,
			PersistentIdentifier persistentIdentifier) throws EdalException {

		if (getImplProv() == null || getSubject() == null) {
			throw new EdalException(
					"unable to load entity : start eDAL system first");
		}
		return DataManager.getImplProv()
				.getPrimaryDataEntityForPersistenIdentifier(uuid,
						versionNumber, persistentIdentifier);
	}

	/**
	 * Getter for a {@link PrimaryDataEntity} for the review process to present
	 * it to a reviewer.
	 * 
	 * @param uuid
	 *            the ID of the searched {@link PrimaryDataEntity}.
	 * @param versionNumber
	 *            the version number of the {@link PrimaryDataEntity}.
	 * @param internalId
	 *            the internal ID of the corresponding
	 *            {@link de.ipk_gatersleben.bit.bi.edal.primary_data.file.PublicReference}
	 * @param reviewerCode
	 *            the ID to identify the reviewer.
	 * @return the {@link PrimaryDataEntity}
	 * @throws EdalException
	 *             if unable to load the {@link PrimaryDataEntity}.
	 */
	static PrimaryDataEntity getPrimaryDataEntityForReviewer(String uuid,
			long versionNumber, String internalId, int reviewerCode)
			throws EdalException {
		if (getImplProv() == null || getSubject() == null) {
			throw new EdalException(
					"unable to load Entity : start eDAL system first");
		}
		return DataManager.getImplProv().getPrimaryDataEntityForReviewer(uuid,
				versionNumber, internalId, reviewerCode);
	}

	/**
	 * Static function to get the root {@link PrimaryDataDirectory} of the
	 * eDAL-System.
	 * 
	 * @param implementationProvider
	 *            must provide the implementing classes the implementation,
	 *            which will be used. The call pass the current logged in JAAS
	 *            subject.For example:
	 *            <p>
	 * 
	 *            <pre>
	 * ImplementationProvider myImpl = new MyEDALImplementation();
	 * LoginContext CTX = new LoginContext(...);
	 *        CTX();
	 *        Subject mySubject = CTX.getSubject();
	 * PrimaryDataDirectory root_dir = DataManager.getRootDirectory(myImpl, mySubject);
	 * </pre>
	 * @param subject
	 *            the authenticated subject
	 * @return the root {@link PrimaryDataDirectory} for the passed
	 *         implementation
	 * @throws PrimaryDataDirectoryException
	 *             if unable to create
	 *             {@link de.ipk_gatersleben.bit.bi.edal.primary_data.metadata.MetaData}
	 *             instance or if unable to initialize security system.
	 */
	public static PrimaryDataDirectory getRootDirectory(
			final ImplementationProvider implementationProvider,
			final Subject subject) throws PrimaryDataDirectoryException {

		if (subject == null) {
			throw new PrimaryDataDirectoryException(
					"Not allowed to use a null subject !");

		}

		if (DataManager.stopLatch.getCount() == 0) {
			DataManager.stopLatch = new CountDownLatch(1);
		}

		/**
		 * set the current subject and implementation provider in ThreadLocal
		 * variable to bound both to the thread
		 */
		DataManager.threadlocalsubject.set(subject);
		DataManager.implementationprovider = implementationProvider;
		DataManager.configuration = implementationProvider.getConfiguration();

		try {
			/** always add ALLPrincipal as supported Principal */
			implementationProvider.getConfiguration().getSupportedPrincipals()
					.add(ALLPrincipal.class);

		} catch (EdalConfigurationException e) {
			/** shut down the DataManager if no supported principal is defined */
			DataManager.shutdown();
			throw new PrimaryDataDirectoryException(e);
		}

		/**
		 * check if the current ImplementationProvider can create a
		 * MetaDataInstance; check modifier of default constructor
		 */
		try {
			implementationProvider.createMetaDataInstance().getClass()
					.getConstructor((Class<?>[]) null);
		} catch (NoSuchMethodException | SecurityException e) {
			throw new PrimaryDataDirectoryException(
					"Can not create a MetaData instance; change modifier of default constructor",
					e);
		}

		try {
			DataManager.initSecuritySystem(implementationProvider);
		} catch (SecurityException e) {
			throw new PrimaryDataDirectoryException(
					"Can not initialize security system : " + e.getMessage(), e);
		}

		/** start HTTP service to enable external URL access to public entities */
		try {
			DataManager.initHTTPService();
		} catch (EdalException e) {
			throw new PrimaryDataDirectoryException(
					"Can not initialize the HTTP Service : " + e.getMessage(),
					e);
		}
		if (!isConfigurationValid) {
			initOptInProcess();
		}

		/** register graceful shutdown hook at JVM */
		Runtime.getRuntime().addShutdownHook(new ShutdownInterceptor());

		try {
			PrimaryDataDirectory root = PrimaryDataDirectory
					.getRootDirectory(implementationProvider.getConfiguration()
							.getSupportedPrincipals());

			if (!isConfigurationValid) {
				DataManager.checkReviewStatusThread.run();
			}
			isConfigurationValid = true;
			return root;

		} catch (EdalConfigurationException e) {
			throw new PrimaryDataDirectoryException(e.getCause());
		}

	}

	/**
	 * In order to know, who is working with an eDAL data structure instance. We
	 * bind in the
	 * {@link DataManager#getRootDirectory(ImplementationProvider, Subject)}
	 * function the subject as ThreadLocal object to the current thread
	 * 
	 * @return Subject the subject working in its Thread with a eDAL mount
	 */
	public static Subject getSubject() {

		return DataManager.threadlocalsubject.get();
	}

	/**
	 * Getter all supported {@link Principal}s of the current eDAL system.
	 * 
	 * @return the list of supported {@link Principal}s
	 * @throws EdalException
	 *             if unable to load {@link Principal}s.
	 */
	public static List<Class<? extends Principal>> getSupportedPrincipals()
			throws EdalException {
		if (DataManager.getImplProv() == null) {
			throw new EdalException(
					"No ImplementationProvider set -> run getRootDirectory()");
		}
		return DataManager.getImplProv().getSupportedPrincipals();
	}

	/**
	 * Getter for the used space in the mount path of eDAL.
	 * 
	 * @return used space
	 * @throws EdalException
	 *             if no path is specified.
	 */
	public static Long getUsedStorageSpace() throws EdalException {
		if (DataManager.getImplProv() == null) {
			throw new EdalException(
					"No ImplementationProvider set -> run getRootDirectory()");
		}
		return DataManager.getImplProv().getUsedStorageSpace();
	}

	/**
	 * Start the HTTP-Service to make eDAL object available over a HTTP/HTTPS.
	 * 
	 * @throws EdalException
	 *             if unable to initialize the HTTP server.
	 */
	private static void initHTTPService() throws EdalException {

		if (server == null) {
			server = new EdalJettyServer(getConfiguration());
			server.start();
		}
	}

	private static void initOptInProcess() throws PrimaryDataDirectoryException {
		try {
			InternetAddress newRootUser = DataManager.getImplProv()
					.getRootUser();

			InternetAddress previousRootUser = DataManager.getImplProv()
					.getPreviousRootUser();

			/** already ROOT CHEAT used to avoid the double-log-in process */
			if (newRootUser.equals(previousRootUser)) {

				DataManager.getImplProv().getLogger()
						.info("ALREADY ROOT CHEAT USED");

				configuration.setErrorEmailAddress(DataManager.getImplProv()
						.getPreviousRootUser());
			}

			/** new user uses ROOT CHEAT */
			else if (newRootUser.equals(rootCheat)) {
				UUID uuid = UUID.randomUUID();
				DataManager.getImplProv().getLogger().info("ROOT CHEAT");
				try {
					DataManager.getImplProv().storeRootUser(
							DataManager.getSubject(),
							new InternetAddress(ROOT_CHEAT), uuid);
					DataManager.getImplProv().validateRootUser(
							new InternetAddress(ROOT_CHEAT), uuid);

					configuration.setErrorEmailAddress(DataManager
							.getImplProv().getPreviousRootUser());

				} catch (AddressException e) {
					DataManager.getImplProv().getLogger()
							.warn("unable to use ROOT CHEAT:" + e.getMessage());
					System.exit(0);
				}
			}

			/** new root user without ROOT CHEAT */
			else {

				if (previousRootUser == null) {
					DataManager.getImplProv().getLogger()
							.info("no root user defined ! ");

					UUID uuid = UUID.randomUUID();

					DataManager.getImplProv().storeRootUser(
							DataManager.getSubject(), newRootUser, uuid);

					VeloCityHtmlGenerator veloCityHtmlGenerator = new VeloCityHtmlGenerator();

					configuration.setErrorEmailAddress(newRootUser);

					/** send confirmation email */
					sendEmail(
							veloCityHtmlGenerator.generateEmailForDoubleOptIn(
									EdalJettyServer.getServerURL(),
									newRootUser, uuid).toString(),
							"[eDAL-Service]: Double-Opt-In",
							newRootUser.getAddress());

					Long startTime = System.currentTimeMillis();
					DataManager
							.getImplProv()
							.getLogger()
							.info("Waiting for confirmation of root user '"
									+ newRootUser.getAddress() + "'...");
					while (!implementationprovider.isRootValidated(newRootUser)) {
						DataManager
								.getImplProv()
								.getLogger()
								.debug("Waiting for confirmation of root user '"
										+ newRootUser.getAddress() + "'...");

						try {
							Thread.sleep(OPT_IN_INTERVAL);
						} catch (InterruptedException e) {
							throw new PrimaryDataDirectoryException(
									"error while waiting for user opt-in", e);
						}
						if (System.currentTimeMillis() - startTime > OPT_IN_TIMEOUT) {
							DataManager
									.getImplProv()
									.getLogger()
									.error("Timeout for root user confirmation");
							System.exit(0);
						}
					}
				} else {

					if (!newRootUser.equals(previousRootUser)) {

						UUID uuid = UUID.randomUUID();

						DataManager.getImplProv().storeRootUser(
								DataManager.getSubject(), newRootUser, uuid);

						VeloCityHtmlGenerator veloCityHtmlGenerator = new VeloCityHtmlGenerator();

						configuration.setErrorEmailAddress(newRootUser);

						/** send confirmation email */
						sendEmail(
								veloCityHtmlGenerator
										.generateEmailForDoubleOptIn(
												EdalJettyServer.getServerURL(),
												newRootUser, uuid).toString(),
								"[eDAL-Service]: Double-Opt-In",
								newRootUser.getAddress());

						Long startTime = System.currentTimeMillis();
						DataManager
								.getImplProv()
								.getLogger()
								.info("Waiting for confirmation of root user '"
										+ newRootUser.getAddress() + "'...");
						while (!implementationprovider
								.isRootValidated(newRootUser)) {
							DataManager
									.getImplProv()
									.getLogger()
									.debug("Waiting for confirmation of root user '"
											+ newRootUser.getAddress() + "'...");
							try {
								Thread.sleep(OPT_IN_INTERVAL);
							} catch (InterruptedException e) {
								throw new PrimaryDataDirectoryException(
										"error while waiting for user opt-in",
										e);

							}
							if (System.currentTimeMillis() - startTime > OPT_IN_TIMEOUT) {
								DataManager
										.getImplProv()
										.getLogger()
										.error("Timeout for root user confirmation");
								System.exit(0);
							}
						}
						/** send email to the old root user; */
						if (!previousRootUser.getAddress().equals(ROOT_CHEAT)) {
							sendEmail(
									veloCityHtmlGenerator
											.generateEmailForChangedRootUser(
													EdalJettyServer
															.getServerURL(),
													newRootUser,
													previousRootUser)
											.toString(),
									"[eDAL-Server]: notice - root user transfered to "
											+ newRootUser.getAddress(),
									previousRootUser.getAddress());
						}

					} else {
						DataManager.getImplProv().getLogger()
								.info("Root user already registered !");
						configuration.setErrorEmailAddress(previousRootUser);
					}

				}
			}
		} catch (EdalException e) {
			throw new PrimaryDataDirectoryException(
					"unable to load root user, please check database", e);
		}
	}

	/**
	 * Initialize the security system of eDAL.
	 * 
	 * @param implementationProvider
	 *            an {@link ImplementationProvider} that provide all
	 *            implementation classes.
	 * @throws SecurityException
	 *             if unable to find policy file or unable to create a new
	 *             {@link de.ipk_gatersleben.bit.bi.edal.primary_data.security.PermissionProvider}
	 *             instance.
	 */
	private static void initSecuritySystem(
			final ImplementationProvider implementationProvider)
			throws SecurityException {

		if (System.getProperty("java.security.policy") == null) {

			try {
				final String policy = DataManager.class.getResource(
						"policy.txt").toString();
				System.setProperty("java.security.policy", policy);
			} catch (final Exception e) {
				throw new SecurityException("unable to find policy file", e);
			}

			/** start SecurityManager */
			System.setSecurityManager(new SecurityManager());

			EdalPolicy edalPolicy = null;
			try {
				edalPolicy = new EdalPolicy(implementationProvider
						.getPermissionProvider().newInstance());
			} catch (final Exception e) {
				throw new SecurityException(
						"unable to create new PermissionProvider", e);
			}
			final List<Policy> policies = new ArrayList<Policy>(2);

			policies.add(edalPolicy);
			policies.add(Policy.getPolicy());

			Policy.setPolicy(new EdalCompositePolicy(policies));
		}

	}

	/**
	 * Reload all initial default permission in
	 * {@link DataManager#DEFAULT_PERMISSIONS}.
	 */
	public static void resetDefaultPermissions() {
		DataManager.setDefaultPermissions(DataManager.DEFAULT_PERMISSIONS);
	}

	/**
	 * Function to send an eMail to the given recipient.
	 * 
	 * @param emailAddress
	 *            the eMail address of the recipient.
	 */
	private static void sendEmail(final String message, final String subject,
			final String emailAddress) {

		final Properties props = new Properties();
		props.put("mail.smtp.host", DataManager.getConfiguration()
				.getMailSmtpHost());

		final javax.mail.Session session = javax.mail.Session
				.getDefaultInstance(props);

		final Message mail = new MimeMessage(session);

		try {
			InternetAddress addressFrom = new InternetAddress(DataManager
					.getConfiguration().getEdalEmailAddress(), "eDAL-Service <"
					+ DataManager.getConfiguration().getEdalEmailAddress()
					+ ">");

			mail.setFrom(addressFrom);
			final InternetAddress addressTo = new InternetAddress(emailAddress);
			mail.setRecipient(Message.RecipientType.TO, addressTo);
			mail.setSubject(subject);
			mail.setContent(message, "text/html");
			Transport.send(mail);

		} catch (MessagingException | UnsupportedEncodingException e) {
			DataManager.getConfiguration().getErrorLogger()
					.fatal(emailAddress + " : " + e.getMessage());
		}
	}

	/**
	 * Overrides the current default permissions of the current user with the
	 * new permissions.
	 * 
	 * @param newUserPermissions
	 *            the user permissions to set to the default permissions.
	 */
	public static void setDefaultPermissions(
			final Map<Principal, List<Methods>> newUserPermissions) {
		DataManager.threadlocaldefaultpermissions.set(newUserPermissions);
	}

	/**
	 * Setter for the current {@link Subject}.
	 * 
	 * @param subject
	 *            a {@link Subject} object.
	 */
	public static void setSubject(final Subject subject) {
		DataManager.threadlocalsubject.set(subject);
	}

	/**
	 * Convenience function to shutdown the eDAL system.
	 */
	public static void shutdown() {

		implementationprovider.getLogger().info(
				"Trying to shutdown EDALClient...");

		DataManager.checkReviewStatusThread.done();
		DataManager.getImplProv().shutdown();
		if (DataManager.server != null) {
			DataManager.server.stop();
		}
		DataManager.isConfigurationValid = false;

		DataManager.stopLatch.countDown();

		implementationprovider.getLogger().info(
				"EDALClient successfully closed !");

	}

	public static void waitForShutDown() {

		try {
			DataManager.stopLatch.await();
		} catch (InterruptedException e) {
			implementationprovider.getLogger().error(
					"error while count down stopLatch:" + e.getMessage(), e);
		}

	}

}
